package flare.animate
{
  import flash.display.Shape;
  import flash.events.Event;
  import flash.events.TimerEvent;
  import flash.utils.Timer;

  /**
   * Scheduler that oversees animation and time-based processing. Uses an
   * internal timer to regularly invoke the current set of scheduled
   * objects. Typically, interaction with the scheduler is automatically
   * handled by Transition classes. However, custom implmentations of
   * the ISchedulable interface will need to be scheduled. Use the
   * <tt>Scheduler.instance</tt> property, and not the constructor, to get
   * a reference to the active scheduler.
   * 
   * <p>By default, the Scheduler issues updates to all scheduled items each
   * time the Flash Player advances to the next frame, as reported by the
   * <code>Event.ENTER_FRAME</code> event. To instead set the update interval
   * manually, see the <code>timerInterval</code> property.</p>
   */
  public class Scheduler
  {
    private static const _instance:Scheduler = new Scheduler(_Lock);
    /** The default Scheduler instance. */
    public static function get instance():Scheduler
    {
      return _instance;
    }

    private var _scheduled:Vector.<Object> = new Vector.<Object>(0); // list of all currently scheduled items
    private var _ids:Object;                                         // map of all named items
    private var _timer:Timer;                                        // timer for interval-based scheduling
    private var _obj:Shape;                                          // shape for getting ENTER_FRAME events

    /**
     * Sets the timer interval (in milliseconds) at which the scheduler
     * should process events. If this value is greater than zero, a
     * <code>Timer</code> instance will be used to trigger scheduler
     * updates at the given interval. If this value is less than or equal
     * to zero (the default), scheduler updates will be issued with each
     * time the Flash Player advances to the next frame according to the
     * <code>Event.ENTER_FRAME</code> event.
     */
    public function get timerInterval():Number
    {
      return _timer.delay;
    }

    public function set timerInterval(t:Number):void
    {
      pause();
      _timer.delay = (t > 0 ? t : 0);
      play();
    }

    /**
     * Creates a new Scheduler--this constructor should be not used;
     * instead use the <code>instance</code> property.
     * @param lock a lock object to emulate a private constructor
     */
    public function Scheduler(lock:Class)
    {
      if(lock == _Lock)
      {
        _scheduled.length = 0;
        _ids = {
        };

        _obj = new Shape();
        _timer = new Timer(0);
        _timer.addEventListener(TimerEvent.TIMER, tick);
      }

      else
      {
        throw new Error("Invalid constructor. Use Scheduler.instance.");
      }
    }

    /**
     * Plays the scheduler, allowing it to process events.
     */
    private function play():void
    {
      if(timerInterval <= 0)
      {
        if(!_obj.hasEventListener(Event.ENTER_FRAME))
        _obj.addEventListener(Event.ENTER_FRAME, tick);
      }

      else if(!_timer.running)
      {
        _timer.start();
      }
    }

    /**
     * Pauses the scheduler, so that events are not processed.
     */
    private function pause():void
    {
      if(timerInterval <= 0)
      {
        _obj.removeEventListener(Event.ENTER_FRAME, tick);
      }

      else
      {
        _timer.stop();
      }
    }

    /**
     * Adds an object to the scheduling list.
     * @param item a schedulable object to add
     */
    public function add(item:ISchedulable):void
    {
      if(item.id && _ids[item.id] != item)
      {
        cancel(item.id);
        _ids[item.id] = item;
      }
      _scheduled.push(item);
      play();
    }

    /**
     * Removes an object from the scheduling list.
     * @param item the object to remove
     * @return true if the object was found and removed, false otherwise
     */
    public function remove(item:ISchedulable):Boolean
    {
      var idx:int = _scheduled.indexOf(item);

      if(idx >= 0)
      {
        _scheduled.splice(idx, 1);

        if(item.id && _ids[item.id] == item)
        {
          if(_scheduled.indexOf(item) < 0)
          delete _ids[item.id];
        }
      }
      return (idx >= 0);
    }

    /**
     * Indicates if an object with the given id is currently in the
     * scheduler queue. 
     * @param id the id to check for
     * @return true if an object with that id is currently scheduled,
     *  false otherwise
     */
    public function isScheduled(id:String):Boolean
    {
      return _ids[id] != undefined;
    }

    /**
     * Looks up the scheduled object indicated by the given id, if any.
     * @param id the id to lookup
     * @return the scheduled object with matching id, of null if none
     */
    public function lookup(id:String):ISchedulable
    {
      return id == null ? null : _ids[id];
    }

    /**
     * Cancels any scheduled object with a matching id. 
     * @param id the id to cancel
     * @return true if an object was found and cancelled, false otherwise
     */
    public function cancel(id:String):Boolean
    {
      var s:ISchedulable = _ids[id];

      if(s != null)
      {
        remove(s);
        s.cancelled();
        return true;
      }

      else
      {
        return false;
      }
    }

    /**
     * Frame/timer callback that invokes each scheduled object.
     * @param event the event that triggered the callback
     */
    public function tick(event:Event):void
    {
      // all events will see the same timestamp
      var time:Number = new Date().time;

      for each(var s:ISchedulable in _scheduled)
      {
        if(s.evaluate(time))
            remove(s);
      }

      if(_scheduled.length == 0)
      {
        pause();
      }
    }
  } // end of class Scheduler
}

// scheduler lock class to enforce singleton pattern
class _Lock
{
}